iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Modern Web

Laravel 實務筆記系列 第 16

Eloquent ORM - 一對一關聯

  • 分享至 

  • xImage
  •  

Eloquent 可以在 Model 之間建立關聯查詢,這樣可以藉由這些關聯快速查詢出所需的資料。

舉例來說,目前我們在拉 Todo 清單的時候都是整個資料庫翻出來顯示,如果可以建立 User 跟 Todo 之間的一對多關聯,就能夠只取出屬於登入 User 的 Todo 清單。

關聯有許多形式,常見的有一對一,一對多,多對多,也有多角關係的關聯,這邊會一一介紹。

一對一關聯

這段目標是建立一個使用者的設定模型,一個使用者只會有一組設定資料,所以是一對一關聯。

先來建模型

sail artisan make:model UserSetting -mcr

建立 UserSetting 模型的同時一併建立 Migration ,順便加上 Controller。

首先,資料之間要關聯需要額外的欄位作為查詢的依據,所以在 Migration 裡加上 user_id 欄位

/database/migrations/XXXX_XX_XX_XXXXXX_create_user_settings_table.php

@@ -16,6 +16,7 @@ class CreateUserSettingsTable extends Migration
         Schema::create('user_settings', function (Blueprint $table) {
             $table->id();
             $table->timestamps();
+            $table->unsignedBigInteger('user_id');
         });
     }

user_id 的型別要對應到 user 資料的 id 欄位,因為 id() 等於是 unsignedBigInteger 所以這邊就建立 unsignedBigInteger 的欄位。

hasOne 關聯

Eloquent 建立 Model 間關聯的方式是在 Model 類別底下加入對應的函式,然後用像是 hasOne 這類方法建立 Query Builder。

/app/Models/User.php

@@ -8,13 +8,13 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
 use Illuminate\Notifications\Notifiable;
 use Laravel\Sanctum\HasApiTokens;
 use Illuminate\Database\Eloquent\SoftDeletes;
+use App\Models\UserSetting;

@@ -45,4 +45,9 @@ class User extends Authenticatable
     protected $casts = [
         'email_verified_at' => 'datetime',
     ];
+    
+    public function setting()
+    {
+        return $this->hasOne(UserSetting::class);
+    }

setting 這個函式的名稱可以任意設定。

hasOne 方法第一個參數是目標 Model 的類別名稱,UserSetting::class 會以字串的形式回傳該類別包含 namespace 在內的名稱,所以如果不用 use 引入類別的話也可以直接用字串

/app/Models/User.php

@@ -45,4 +45,9 @@ class User extends Authenticatable
     protected $casts = [
         'email_verified_at' => 'datetime',
     ];
+    
+    public function setting()
+    {
+        return $this->hasOne('App\Models\UserSetting');
+    }

如果沒有傳其他參數給 hasOne 的話, Eloquent 會以預設的欄位名稱進行查詢。
像這邊是 User hasOne UserSetting ,就會在 UserSetting 資料表中的 user_id 欄位找對應 user->id 的資料。

如果不是用預設的欄位名稱,也可以自行指定

舉歷來說像是改成在 UserSetting 中找 user_email 對應到 user 的 email 值的資料。

/app/Models/User.php

@@ -45,4 +45,9 @@ class User extends Authenticatable
     protected $casts = [
         'email_verified_at' => 'datetime',
     ];
+    
+    public function setting()
+    {
+        return $this->hasOne('App\Models\UserSetting','user_email','email');
+    }
hasOne(<模型名稱>,<目標模型的外鍵名稱>,<模型關聯鍵名稱>)

belongsTo 關聯

像上面是 "User 擁有一個 UserSetting" 的關聯,關聯用的鍵值是建立在 UserSetting 這邊,如果想要從 UserSetting 逆向找出擁有的 User 是誰,用的是 belongsTo 方法

/app/Models/UserSetting.php

@@ -4,8 +4,14 @@ namespace App\Models;
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use App\Models\User;
 
 class UserSetting extends Model
 {
     use HasFactory;
+
+    public function user()
+    {
+        return $this->belongsTo(User::class);
+    }
 }

用法跟 hasOne 差不多,不過如果要自訂關聯欄位的話順序會反過來變成先寫自己的欄位再寫目標的欄位

 class UserSetting extends Model
 {
     use HasFactory;
+
+    public function user()
+    {
+        return $this->belongsTo(User::class,'user_id','id');
+    }
 }

寫入關聯資料

我們希望當新申請 User 的時候同時建立 UserSetting 資料,所以稍微改一下建立新 user 的流程。

/app/Http/Controllers/Auth/RegisteredUserController.php

@@ -46,6 +46,8 @@ class RegisteredUserController extends Controller
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);
 
+        $user->setting()->create();
+
         event(new Registered($user));

因為在 Model 中建立了 setting 函式,所有 User 實例都會有 setting 這個方法,當呼叫的時候就會去搜尋 UserSetting 中關聯的資料並建成 UserSetting 的實例,這樣就能直接從這個實例上呼叫 create 方法寫入資料,跟之前寫入資料的方法相同。

因為 UserSetting 目前也沒定義其他欄位,只有 id 跟 timeStamps ,所以 create 不用特地帶入參數。

接著重新申請一個帳號,就會發現除了 users 外 user_settings 資料表也有新的資料。

讀取關聯資料

有了資料之後要建立對應的方法把它讀出來,在 RegisteredUserController 建立新的函式

/app/Http/Controllers/Auth/RegisteredUserController.php

@@ -67,4 +69,13 @@ class RegisteredUserController extends Controller
 
         return redirect('/');
     }
+
+    public function setting(Request $request)
+    {
+        $user = Auth::user(); 
+
+        return $user->setting;
+    }
 }

跟上一篇一樣用 Auth::user() 取得登入的 User。

接著 $user->setting 就可以直接取得對應的 UserSetting 資料了。

$user->setting 跟 $user->setting() 的用法可能會搞混,比較一下。

$user->setting() 會建立 Query Builder ,所以可以串接 create , update 這類方法做資料的更新。

如果要取得資料的可以串接 first 或 get,注意兩者是有差別的, first 會取出單筆資料所以回傳的是單一物件,但 get 是用來取得一組資料的所以回傳會是陣列,然後裡面只有一個物件。

至於 $user->setting ,可以視為 $user->setting()->first(); 的簡易寫法,因為用的是一對一關聯的 hasOne 方法,所以回傳會只有一個物件。

$user->setting 的回傳

$user->setting()->get() 的回傳,多包了一層陣列

可以多加一個路由來看這些回傳值

/routes/auth.php b/routes/auth.php

@@ -66,3 +66,5 @@ Route::post('/logout', 
+                
+Route::get('/setting', [RegisteredUserController::class,'setting'])->middleware('auth');

登入之後在瀏覽器輸入 localhost/setting

Eager Loading

目前上面的路由是用來只取出 UserSetting 資料的,不過一般都會是在取得 User 的同時,將 UserSetting 作為 User 資料的一部分取出。以 SQL 來說就是用 Join 一併載入關聯資料。

在 Eloquent 中是以 with 方法來在模型上加載關聯的資料。

with 方法是 Query Builder 的一種,所以後面要再加 first() 或 get() 才能取得資料。

@@ -73,8 +73,8 @@ class RegisteredUserController extends Controller
 
     public function setting(Request $request)
     {
-        $user = Auth::user(); 
+        $user = Auth::user()->with('setting')->first(); 
 
-        return $user->setting;
+        return $user;
     }
 }

這樣就能看到 setting 的資料被加入到 User 的資料中回傳了

資料庫關聯

上面建立的關聯都是 ORM 層級的,在資料庫上沒有建成關聯,如果要加上資料庫層的關聯限制的話,要用 migration。

/database/migrations/XXXX_XX_XX_XXXXXX_create_user_settings_table.php

@@ -16,7 +16,9 @@ class CreateUserSettingsTable extends Migration
         Schema::create('user_settings', function (Blueprint $table) {
             $table->id();
             $table->timestamps(); 
             $table->unsignedBigInteger('user_id');
+            
+            $table->foreign('user_id')->references('id')->on('users');
         });
     }

Eloquent ORM 的關聯跟資料庫的關聯並沒有直接的關係,只是一種方便的查詢手段。所以就算資料庫層沒有建立關聯,還是可以用 Model 的關聯查資料,只是資料表要有對應的欄位。


上一篇
Eloquent ORM - 軟刪除
下一篇
Eloquent ORM - 一對多關聯
系列文
Laravel 實務筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言